-module ( vis ).
-export( [ start/1, stop/1, addLine/6, addCircle/5, addText/5, addRect/6, addSquare/5, addDiamond/5, remove/3, remove/2, clear/1 ] ).

%% wx_object callbacks
-export([init/1, terminate/2,  code_change/3,
	 handle_info/2, handle_call/3, handle_event/2, handle_sync_event/3]).

-include_lib("wx/include/wx.hrl").
-behaviour(wx_object).

-record( state, { bounds={0,0,1,1}, wnd={}, geom=[], grid=[], pens=[], font, nullBrush } ).



start( Bounds ) ->
  spawn( fun() -> loopStart( Bounds ) end ).

  
stop( Vis ) ->
  Vis ! { call, stop_vis },
  true.
  
  
addLine( Vis, X0, Y0, X1, Y1, Options )
  when is_number(X0), is_number (Y0), is_number( X1 ), is_number(Y1), is_list(Options) ->
  Vis ! { call, { add_line, X0, Y0, X1, Y1, Options } },
  true.

  
addCircle( Vis, X, Y, R, Options )
  when is_number(X), is_number(Y), is_number(R), is_list(Options) ->
  Vis ! { call, { add_circle, X, Y, R, Options } },
  true.
  
  
addText( Vis, X, Y, Text, Options )
  when is_number(X), is_number(Y), is_list(Options) ->
  Vis ! { call, { add_text, X, Y, Text, Options } },
  true.
  
  
addRect( Vis, X0, Y0, X1, Y1, Options ) ->
  addLine( Vis, X0, Y0, X1, Y0, Options ),
  addLine( Vis, X0, Y1, X1, Y1, Options ),
  addLine( Vis, X0, Y0, X0, Y1, Options ),
  addLine( Vis, X1, Y0, X1, Y1, Options ).
  
  
  
addSquare( Vis, X, Y, S, Options ) ->
  HS = S / 2,
  addRect( Vis, X-HS, Y-HS, X+HS, Y+HS, Options ).
  

  
addDiamond( Vis, X, Y, S, Options ) ->
  HS = S / 2,
  addLine( Vis, X-HS, Y, X, Y+HS, Options ),
  addLine( Vis, X, Y+HS, X+HS, Y, Options ),
  addLine( Vis, X+HS, Y, X, Y-HS, Options ),
  addLine( Vis, X, Y-HS, X-HS, Y, Options ).
  
  
  
remove( Vis, Tag1, Tag2 ) ->
  Vis ! { call, { remove, { Tag1, Tag2 } } },
  true.


remove( Vis, Tag1 ) ->
  Vis ! { call, { remove, { Tag1, undefined } } },
  true.

  
clear( Vis ) ->
  Vis ! { call, clear },
  true.

  
  
%control process
loopStart( Bounds ) ->
  WX = wx:new(),
  Ref = wx_object:start_link( ?MODULE, {WX, Bounds}, [] ),
  loop( Ref ).
  
  
loop( Ref ) ->
  receive
    { call, Args } ->
      wx_object:call( Ref, Args );
    Other ->
      io:format( "Unknown vis message: ~p\n", [Other] )
  end,
  loop( Ref ).
  




%% wx_object callback
init( {WX, Bounds} ) ->
  io:format( "VIS init: ~p\n", [Bounds] ),
  wx:batch( fun() -> do_init( WX, Bounds ) end ).



do_init( WX, Bounds ) ->
  Wnd = wxFrame:new( WX, -1, "Debug Visualization", [ { size, { 800, 600 } }, { style, ?wxDEFAULT_FRAME_STYLE bor ?wxFULL_REPAINT_ON_RESIZE } ] ),
  wxWindow:connect( Wnd, paint, [callback] ),
  wxPanel:connect( Wnd, size ),
  wxWindow:show( Wnd ),
  { Xmin, Ymin, Xmax, Ymax } = Bounds,
  GridTags = { undefined, undefined },
  Sett = { black }, TextSett = { black },
  Guides = [
      { GridTags, Sett, { line, Xmin, Ymin, Xmax, Ymin } },
      { GridTags, Sett, { line, Xmin, Ymax, Xmax, Ymax } },
      { GridTags, Sett, { line, Xmin, Ymin, Xmin, Ymax } },
      { GridTags, Sett, { line, Xmax, Ymin, Xmax, Ymax } },
      { GridTags, Sett, { line, Xmin, clamp( 0, Ymin, Ymax ), Xmax, clamp( 0, Ymin, Ymax ) } },
      { GridTags, Sett, { line, clamp( 0, Xmin, Xmax ), Ymin, clamp( 0, Xmin, Xmax ), Ymax } },
      { GridTags, TextSett, { text, Xmin, clamp( 0, Ymin, Ymax ), left, bottom, io_lib:format( "~.2f", [Xmin] ) } },
      { GridTags, TextSett, { text, Xmax, clamp( 0, Ymin, Ymax ), right, bottom, io_lib:format( "~.2f", [Xmax] ) } },
      { GridTags, TextSett, { text, clamp( 0, Xmin, Xmax ), Ymin, left, bottom, io_lib:format( "~.2f", [Ymin] ) } },
      { GridTags, TextSett, { text, clamp( 0, Xmin, Xmax ), Ymax, left, top, io_lib:format( "~.2f", [Ymax] ) } }
    ],
  F = wxFont:new( 10, ?wxFONTFAMILY_DEFAULT, ?wxFONTSTYLE_NORMAL, ?wxNORMAL, [] ),
  NullBrush = wxBrush:new( {0,0,0,255}, [ {style, ?wxTRANSPARENT} ] ),
  %FIXME: We never free resources
  { Wnd, #state{ bounds=Bounds, wnd=Wnd, grid=Guides, pens=create_pens(), font=F, nullBrush=NullBrush } }.

clamp( X, Min, Max ) -> min( max( X, Min ), Max ).


handle_sync_event(#wx{event = #wxPaint{}}, _wxObj, #state{}=State ) ->
  paint( State ),
  ok.


handle_event( #wx{ event = #wxSize{} }, State = #state{ wnd=Wnd } ) ->
  wxWindow:refresh( Wnd, [ {eraseBackground, true} ] ),
  { noreply, State };
handle_event( _WX, State ) ->
  { noreply, State }.

  
  
handle_info( _Msg, State ) ->
  {noreply, State}.



handle_call( { add_line, X0, Y0, X1, Y1, Options }, _From, State ) ->
  H = { parse_tags( Options ), parse_settings( Options ), { line, X0, Y0, X1, Y1 } },
  wxWindow:refresh( State#state.wnd ),
  { reply, true, State#state{ geom = [H | State#state.geom] } };
  
handle_call( { add_circle, X, Y, R, Options }, _From, State ) ->
  H = { parse_tags( Options ), parse_settings( Options ), { circle, X, Y, R } },
  wxWindow:refresh( State#state.wnd ),
  { reply, true, State#state{ geom = [H | State#state.geom] } };

handle_call( { add_text, X, Y, Text, Options }, _From, State ) ->
  HJust = case proplists:get_value( halign, Options ) of
    left -> left; right -> right; undefined -> center
  end,
  VJust = case proplists:get_value( valign, Options ) of
    top -> top; bottom -> bottom; undefined -> center
  end,
  H = { parse_tags( Options ), parse_settings( Options ), { text, X, Y, HJust, VJust, Text } },
  wxWindow:refresh( State#state.wnd ),
  { reply, true, State#state{ geom = [H | State#state.geom] } };

handle_call( { remove, Tags }, _From, State ) ->
  NewGeom = [ { T, Sett, Data } || { T, Sett, Data } <- State#state.geom, not tags_pair_match( Tags, T ) ],
  wxWindow:refresh( State#state.wnd ),
  { reply, true, State#state{ geom=NewGeom } };
  
handle_call( clear, _From, State ) ->
  wxWindow:refresh( State#state.wnd ),
  { reply, true, State#state{ geom=[] } };

handle_call( stop_vis, _From, State ) ->
  { stop, normal, true, State }.

  
  
code_change(_, _, State) -> {stop, ignore, State}.


terminate( _Reason, #state{ wnd=Wnd } ) ->
  wxWindow:destroy( Wnd ),
  ok.



%%% Drawing %%%

parse_tags( Options ) ->
  { proplists:get_value( tag1, Options ), proplists:get_value( tag2, Options ) }.
  
  
parse_settings( Options ) ->
  { proplists:get_value( color, Options, black ) }.

  
  
colors() -> [
    { black,    { 0, 0, 0 } },
    { white,    { 255, 255, 255 } },
    { red,      { 255, 0, 0 } },
    { green,    { 0, 255, 0 } },
    { blue,     { 0, 0, 255 } },
    { yellow,   { 255, 255, 0 } },
    { magenta,  { 255, 0, 255 } },
    { cyan,     { 0, 255, 255 } }
  ].

  

create_pens() ->
  C = colors(),
  [ { Name, wxPen:new( RGB, [ { width, 0 } ] ) } || { Name, RGB } <- C ].


  
tags_pair_match( { SearchTag1, SearchTag2 }, { ItemTag1, ItemTag2 } ) -> 
tags_match( SearchTag1, ItemTag1 ) andalso tags_match( SearchTag2, ItemTag2 ).

tags_match( undefined, _ItemTag ) -> true;
tags_match( _SearchTag, undefined ) -> false;
tags_match( SearchTag, ItemTag ) -> ( SearchTag =:= ItemTag ).


paint( #state{ bounds=Bounds, wnd=Wnd, geom=Geom, grid=Grid, pens=Pens, font=Font, nullBrush=NullBrush } ) ->
  { SX, SY } = wxWindow:getClientSize( Wnd ),
  ExtBounds = extend_bounds( Bounds, 0.03 ),
  { XMIN, YMIN, XMAX, YMAX } = geom_bounds( Geom, ExtBounds ),
  K = min( SX / ( XMAX - XMIN ), SY / ( YMAX - YMIN ) ),
  X0 = 0.5 * ( XMIN + XMAX ) - 0.5 * SX / K,
  Y0 = 0.5 * ( YMIN + YMAX ) - 0.5 * SY / K,
  DC = wxPaintDC:new( Wnd ),
  wxDC:setBrush( DC, NullBrush ),
  wxDC:setFont( DC, Font ),
  draw_geom( DC, Grid, { X0, Y0 }, K, { SX, SY }, Pens ),
  draw_geom( DC, Geom, { X0, Y0 }, K, { SX, SY }, Pens ),
  wxPaintDC:destroy( DC ),
  true.

extend_bounds( { X0, Y0, X1, Y1 }, K ) ->
  DX = X1 - X0,
  DY = Y1 - Y0,
  { X0 - DX * K, Y0 - DY * K, X1 + DX * K, Y1 + DY * K }.
  
  
geom_bounds( [], B ) -> B;
geom_bounds( [ { _Tags, _Sett, Primitive } | T ], { Bminx, Bminy, Bmaxx, Bmaxy } ) ->
  { Xmin, Ymin, Xmax, Ymax } = primitive_bounds( Primitive ),
  geom_bounds( T, { min( Xmin, Bminx ), min( Ymin, Bminy ), max( Xmax, Bmaxx ), max( Ymax, Bmaxy ) } ).



primitive_bounds( { line, X0, Y0, X1, Y1 } ) ->
  { min( X0, X1 ), min( Y0, Y1 ), max( X0, X1 ), max( Y0, Y1 ) };
  
primitive_bounds( { circle, X, Y, R } ) ->
  { X - R, Y - R, X + R, Y + R };
  
primitive_bounds( { text, X, Y, _HJust, _VJust, _Text } ) ->
  { X, Y, X, Y }.

  
  
draw_geom( DC, Geom, MIN, K, ScreenSize, Pens ) ->
  [ draw_primitive( DC, Sett, Primitive, MIN, K, ScreenSize, Pens ) || { _Tags, Sett, Primitive } <- Geom ].
  
  
  
draw_primitive( DC, { Color }, { line, X0, Y0, X1, Y1 }, MIN, K, ScreenSize, Pens ) ->
  case proplists:get_value( Color, Pens ) of
    undefined -> true;
    Pen -> wxDC:setPen( DC, Pen )
  end,
  L = [ convert( X0, Y0, MIN, K, ScreenSize ), convert( X1, Y1, MIN, K, ScreenSize ) ],
  wxDC:drawLines( DC, L );

draw_primitive( DC, { Color }, { circle, X, Y, R }, MIN, K, ScreenSize, Pens ) ->
  case proplists:get_value( Color, Pens ) of
    undefined -> true;
    Pen -> wxDC:setPen( DC, Pen )
  end,
  wxDC:drawCircle( DC, convert( X, Y, MIN, K, ScreenSize ), round( R * K ) );

draw_primitive( DC, { Color }, { text, X, Y, HJust, VJust, Text }, MIN, K, ScreenSize, _Pens ) ->
  {R,G,B} = proplists:get_value( Color, colors(), { 255, 255, 255 } ),
  wxDC:setTextForeground( DC, {R,G,B,255} ),
  { XX, YY } = convert( X, Y, MIN, K, ScreenSize ),
  { XSize, YSize, _Descent, _ExtLeading } = wxDC:getTextExtent( DC, Text, [] ),
  SX = case HJust of
    left -> XX; center -> XX - XSize div 2; right -> XX - XSize
  end,
  SY = case VJust of
    top -> YY; center -> YY - YSize div 2; bottom -> YY - YSize
  end,
  wxDC:drawText( DC, Text, { SX, SY } ).



convert( X, Y, { XMIN, YMIN }, K, { _SX, SY } ) -> { round( K * ( X - XMIN ) ), round( SY - K * ( Y - YMIN ) ) }.
